/*
 *  NFCSigning - Open source library for signing/validation of NDEF messages
 *  Copyright (C) 2009-2010 The NFCSigning Team
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */
package sampleappb;

import com.ericsson.nfc.signing.CertificateFetcher;
import com.ericsson.nfc.signing.FetcherException;
import com.ericsson.nfc.signing.MessageValidator;
import com.ericsson.nfc.signing.Signature;
import com.ericsson.nfc.signing.SignatureFetcher;
import com.ericsson.nfc.signing.SignedMessage;
import com.ericsson.nfc.signing.certificate.Certificate;
import com.ericsson.nfc.signing.certificate.CertificateChain;
import com.ericsson.nfc.signing.certificate.X509Certificate;
import com.ericsson.nfc.signing.certificate.X509CertificateChain;
import com.ericsson.nfc.signing.record.URIRecord;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import javax.microedition.contactless.ContactlessException;
import javax.microedition.contactless.DiscoveryManager;
import javax.microedition.contactless.ndef.NDEFMessage;
import javax.microedition.contactless.ndef.NDEFRecord;
import javax.microedition.contactless.ndef.NDEFRecordListener;
import javax.microedition.contactless.ndef.NDEFRecordType;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.io.Connector;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.ItemCommandListener;
import javax.microedition.lcdui.StringItem;
import javax.microedition.midlet.*;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.util.encoders.Base64;

/**
 * @author emarkki
 */
public class ReaderMidlet extends MIDlet implements CommandListener, NDEFRecordListener {

    private static final NDEFRecordType SMARTPOSTER = 
            new NDEFRecordType(NDEFRecordType.NFC_FORUM_RTD, "urn:nfc:wkt:Sp");

    private boolean midletPaused;
    private DiscoveryManager discoveryManager;
    private Form form;
    private X509Certificate[] trustedCerts;
    private Form formWait;
    private Command commandExit;
    private Command commandClose;
    private Command commandClean;

    private final Vector incoming = new Vector();
    private Worker worker = new Worker(incoming);
    private Displayable currentDisplayable;

    public void startApp() {
        if (midletPaused) {
            resumeMIDlet ();
        } else {
            initialize ();
            startMIDlet ();
        }
        midletPaused = false;
    }

    private void initialize() {
    }

    private void startMIDlet() {
        try {

            discoveryManager = DiscoveryManager.getInstance();
            discoveryManager.addNDEFRecordListener(this, SMARTPOSTER);
            
            switchDisplayable(null, getForm());

        } catch (ContactlessException ex) {
            ex.printStackTrace();
            switchDisplayable(new Alert("Discovery Manager",
                    "Could not initialize discover manager to listen for SmartPosters.",
                    null,
                    AlertType.ERROR), getForm());
        }
    }

    public void pauseApp() {
        midletPaused = true;
    }

    public void resumeMIDlet() {
    }

    public void destroyApp(boolean unconditional) {
        if (discoveryManager != null) {
            discoveryManager.removeNDEFRecordListener(this, SMARTPOSTER);
        }
        worker.stop();
    }

    public void exitMIDlet() {
        switchDisplayable(null, null);
        destroyApp(true);
        notifyDestroyed();
    }

    private Form getForm() {
        if(form == null) {
            form = new Form("Reader");
            commandExit = new Command ("Exit", Command.EXIT, 0);
            form.addCommand(commandExit);
            commandClean = new Command ("Clean", Command.SCREEN, 10);
            form.addCommand(commandClean);
            form.setCommandListener(this);
        }
        return form;
    }

    private Form getFormWait() {
        if(formWait == null) {
            Gauge gauge = new Gauge("Please wait", false, Gauge.INDEFINITE, Gauge.CONTINUOUS_RUNNING);
            formWait = new Form("Verifying message...", new Item[]{gauge});
            commandClose = new Command ("Close", Command.BACK, 0);
            formWait.addCommand(commandClose);
            formWait.setCommandListener(this);
        }
        return formWait;
    }

    public void recordDetected(NDEFMessage ndefMessage) {
        System.out.println("recordDetected");

        switchDisplayable(null, getFormWait());

        synchronized(incoming) {
            incoming.addElement(ndefMessage);
            incoming.notify();
        }
    }

    private String dump(NDEFMessage msg) {
        StringBuffer sb = new StringBuffer();

        sb.append("Msg[");
        NDEFRecord[] rs = msg.getRecords();
        for(int i = 0; i < rs.length; i++) {
            sb.append(dump(rs[i]));
            if(i != rs.length-1) {
                sb.append(",");
            }
        }
        sb.append("]");

        return sb.toString();
    }

    private String dump(NDEFRecord record) {
        StringBuffer sb = new StringBuffer();


        sb.append(record.getRecordType().getName());
        sb.append("(");
        // TODO nested payload
        if(record.getRecordType().equals(SMARTPOSTER)) {
            sb.append(dump(record.getNestedNDEFMessage(0)));
        }
        sb.append(")");

        return sb.toString();
    }

    private NDEFRecord findFirstRecord(NDEFMessage msg, NDEFRecordType type) {
        return findFirstRecord(msg.getRecords(), type);
    }

    private NDEFRecord findFirstRecord(NDEFRecord[] records, NDEFRecordType type) {

        int i = findFirstRecordIndex(records, type);

        if(i == -1) {
            return null;
        }

        return records[i];
    }

    private int findFirstRecordIndex(NDEFRecord[] records, NDEFRecordType type) {

        for(int i = 0; i < records.length; i++) {
            if(type.equals(records[i].getRecordType())) {
                return i;
            }
        }

        return -1;
    }

    private static int[] findAllRecordIndexes(NDEFRecord[] records, NDEFRecordType type) {
        Vector indexes = new Vector();

        for(int i = 0; i < records.length; i++) {
            if(type.equals(records[i].getRecordType())) {
                indexes.addElement(new Integer(i));
            }
        }

        int[] result = new int[indexes.size()];
        for(int i = 0; i < result.length; i++) {
            result[i] = ((Integer)indexes.elementAt(i)).intValue();
        }
        return result;
    }

    private Certificate[] getTrustedCerts() {
        if(trustedCerts == null) {
            try {
                trustedCerts = new X509Certificate[] {
                    new X509Certificate(getClass().getResourceAsStream("files/a-root-ca.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/b-root-ca.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/c-root-ca.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/d-root-ca.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/SwedenRootCA.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/NordicRootCA.cer")),
                    new X509Certificate(getClass().getResourceAsStream("files/swiss-root-pub.cer"))
                };
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return trustedCerts;
    }

    public void commandAction(Command command, Displayable displayable) {
        if(displayable == form) {
            if(command == commandExit) {
                exitMIDlet();
            } else if(command == commandClean) {
                getForm().deleteAll();
            }

        } else if(displayable == formWait) {
            if(command == commandClose) {
                switchDisplayable(null, getForm());
            }
        }
    }

    public void switchDisplayable(Alert alert, Displayable nextDisplayable) {
        Display display = Display.getDisplay(this);
        if (alert == null) {
            display.setCurrent(nextDisplayable);
        } else {
            display.setCurrent(alert, nextDisplayable);
        }
        currentDisplayable = nextDisplayable;
    }

    private class MySignatureWebFetcher implements SignatureFetcher {

        public byte[] fetchSignature(URIRecord uri) throws FetcherException {

            // TODO: Security: limit what types of URI:s that are allowed

            InputStream in = null;
            try {
                in = Connector.openInputStream(uri.getURI());
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                int ch;
                while ((ch = in.read()) != -1) {
                    out.write(ch);
                }

                return out.toByteArray();
            } catch (IOException ex) {
                ex.printStackTrace();
                Alert alert = new Alert("Connection Error", "Error fetching signature: "+ex.getMessage(), null, AlertType.ERROR);
                alert.setTimeout(Alert.FOREVER);
                switchDisplayable(alert, getForm());
                return null;
            } finally {
                try {
                    in.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        
    }

    private class MyCertificateFetcher implements CertificateFetcher {

        public CertificateChain fetchChain(URIRecord chainURI) throws FetcherException {

            // TODO: Security: limit what types of URI:s that are allowed

            Vector certsFromUri = new Vector();

            InputStream in = null;
            try {
                in = Connector.openInputStream(chainURI.getURI());

                 int ch = 0;
                 while(ch != -1) {
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    while ((ch = in.read()) != -1 && ch != '\r' && ch != '\n') {
                        out.write(ch);
                    }
                    byte[] line = out.toByteArray();
                    certsFromUri.addElement(new X509Certificate(Base64.decode(line)));

                    if(ch == '\r') {
                        in.skip(1);
                    }
                 }
                 
            } catch(IOException ex) {
                ex.printStackTrace();
                Alert alert = new Alert("Connection Error", "Error fetching certificates: "+ex.getMessage(), null, AlertType.ERROR);
                alert.setTimeout(Alert.FOREVER);
                switchDisplayable(alert, getForm());
                return null;
            } finally {
                if(in != null) {
                    try {
                        in.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }

            X509Certificate[] certs = new X509Certificate[certsFromUri.size()];
            for(int i = 0; i < certs.length; i++) {
                certs[i] = (X509Certificate) certsFromUri.elementAt(i);
            }

            return new X509CertificateChain(certs);
        }
        
    }

    public static String getCommonName(X509Name name) {
        String s = name.toString();
        if(s.indexOf("CN=") != -1) {
            s = s.substring(s.indexOf("CN=")+3);
            if(s.indexOf(",") != -1) {
                s = s.substring(0, s.indexOf(","));
            }
        }
        return s;
    }

    private class Worker implements Runnable {

        private boolean stop;
        private final Vector incoming;

        public Worker(Vector incoming) {
            this.incoming = incoming;
            new Thread(this).start();
        }

        public void run() {

            while(!stop) {

                NDEFMessage message = null;

                synchronized(incoming) {
                    while(!stop && incoming.isEmpty()) {
                        try {
                            incoming.wait();
                        } catch (InterruptedException ex) {}
                    }

                    if(!incoming.isEmpty()) {
                        message = (NDEFMessage) incoming.elementAt(0);
                        incoming.removeElementAt(0);
                    }
                }

                if(message != null) {
                    handleMessage(message);
                }

                if(incoming.isEmpty()) {
                    if(currentDisplayable == formWait) {
                        switchDisplayable(null, form);
                    }
                }
            }
            System.out.println("Worker ended");

        }

        public void stop() {
            synchronized(incoming) {
                stop = true;
                incoming.notify();
            }
        }

        private void handleMessage(NDEFMessage message) {
            try {
                System.out.println(dump(message));

                NDEFRecord r = findFirstRecord(message, SMARTPOSTER);

                if (r == null) {
                    getForm().append("No SmartPoster found.\n");
                    return;
                }
                System.out.println("found sp");

                NDEFMessage sp = r.getNestedNDEFMessage(0);

                // TODO: perhaps accept other record types such as Text, Action etc.

                int[] interesting = findAllRecordIndexes(sp.getRecords(), URIRecord.RECORD_TYPE);

                if (interesting.length == 0) {
                    getForm().append("No URI in SmartPoster.\n");
                    return;
                }
                System.out.println("found uri");

                Certificate[] trustedCerts = getTrustedCerts();

                MessageValidator validatorInfo = new MessageValidator(trustedCerts, new MySignatureWebFetcher(), new MyCertificateFetcher());

                SignedMessage results = validatorInfo.validate(r.getNestedNDEFMessage(0), interesting);

                for (int i = 0; i < interesting.length; i++) {
                    Signature signature = results.getRecordsSignature(interesting[i]);
                    NDEFRecord record = sp.getRecord(interesting[i]);

                    URIRecord uri = new URIRecord(record.getPayload(), 0);

                    final String theUri = uri.getURI();
                    String title;
                    
                    if (signature == null) {
                        title = "Warning: No signature\n";
                    } else if (!signature.getStatus().isValid()) {
                        title = "Warning: Invalid signature, " + signature.getStatus().getErrorMessage()+"\n";
                    } else {
                        title = "Valid signature";
                        if (signature.getStatus().getCertificateChain().getUserCertificate() instanceof X509Certificate) {
                            title += " from " + getCommonName(((X509Certificate) signature.getStatus().getCertificateChain().getUserCertificate()).getStructure().getSubject());
                        }
                    }

                    StringItem uriItem = new StringItem(title+"\n", theUri, Item.HYPERLINK);
                    uriItem.setDefaultCommand(new Command("Go", Command.ITEM, 1));
                    uriItem.setItemCommandListener(new ItemCommandListener() {

                        public void commandAction(Command arg0, Item arg1) {
                            try {
                                System.out.println("Go: " + theUri);
                                boolean mustExit = platformRequest(theUri);
                                if(mustExit) {
                                    exitMIDlet();
                                }
                            } catch (ConnectionNotFoundException ex) {
                                ex.printStackTrace();
                                switchDisplayable(new Alert("Connection error", ex.getMessage(), null, AlertType.ERROR), getForm());
                            }
                        }
                    });

                    uriItem.setLayout(Item.LAYOUT_2 | Item.LAYOUT_LEFT | Item.LAYOUT_NEWLINE_AFTER);
                    getForm().insert(0, uriItem);

                }
                System.out.println("DONE");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}
